/*-------------------------------------------------------------------------
 *
 * tzparser.c
 *	  解析时区偏移文件的函数
 *
 * 注意：此代码是通过GUC变量timezone_abbreviations的check_hook
 * 调用的。因此，它应该使用GUC_check_errmsg()和相关函数报告问题，
 * 尝试避免抛出elog(ERROR)。目前这并不是完全可靠的——特别是在
 * 内存不足的情况下将抛出错误。如果需要，可能可以通过PG_TRY解决。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, 加利福尼亚大学董事会
 *
 * IDENTIFICATION
 *	  src/backend/utils/misc/tzparser.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <ctype.h>

#include "miscadmin.h"
#include "storage/fd.h"
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/tzparser.h"


#define WHITESPACE " \t\n\r"

static bool fc_validateTzEntry(tzEntry *fc_tzentry);
static bool fc_splitTzLine(const char *fc_filename, int fc_lineno,
						char *fc_line, tzEntry *fc_tzentry);
static int	fc_addToArray(tzEntry **fc_base, int *fc_arraysize, int fc_n,
					   tzEntry *fc_entry, bool fc_override);
static int	fc_ParseTzFile(const char *fc_filename, int fc_depth,
						tzEntry **fc_base, int *fc_arraysize, int fc_n);


/*
 * 对tzEntry应用额外的验证检查
 *
 * 如果正常返回true，否则返回false
 */
static bool fc_validateTzEntry(tzEntry *fc_tzentry)
{
	unsigned char *fc_p;

	/*
	 * 检查datetktbl存储格式施加的限制（见datetime.c）
	 */
	if (strlen(fc_tzentry->abbrev) > TOKMAXLEN)
	{
		GUC_check_errmsg("time zone abbreviation \"%s\" is too long (maximum %d characters) in time zone file \"%s\", line %d",
						 fc_tzentry->abbrev, TOKMAXLEN,
						 fc_tzentry->filename, fc_tzentry->lineno);
		return false;
	}

	/*
	 * 验证偏移量：不应超过14小时
	 */
	if (fc_tzentry->offset > 14 * 60 * 60 ||
		fc_tzentry->offset < -14 * 60 * 60)
	{
		GUC_check_errmsg("time zone offset %d is out of range in time zone file \"%s\", line %d",
						 fc_tzentry->offset,
						 fc_tzentry->filename, fc_tzentry->lineno);
		return false;
	}

	/*
	 * 将缩写转换为小写字母（必须与datetime.c的转换匹配）
	 */
	for (fc_p = (unsigned char *) fc_tzentry->abbrev; *fc_p; fc_p++)
		*fc_p = pg_tolower(*fc_p);

	return true;
}

/*
 * 尝试将行解析为时区缩写规格
 *
 * 有效格式为：
 *	name  zone
 *	name  offset  dst
 *
 * 如果正常返回true，否则返回false；数据存储在*tzentry中
 */
static bool fc_splitTzLine(const char *fc_filename, int fc_lineno, char *fc_line, tzEntry *fc_tzentry)
{
	char	   *fc_abbrev;
	char	   *fc_offset;
	char	   *fc_offset_endptr;
	char	   *fc_remain;
	char	   *fc_is_dst;

	fc_tzentry->lineno = fc_lineno;
	fc_tzentry->filename = fc_filename;

	fc_abbrev = strtok(fc_line, WHITESPACE);
	if (!fc_abbrev)
	{
		GUC_check_errmsg("missing time zone abbreviation in time zone file \"%s\", line %d",
						 fc_filename, fc_lineno);
		return false;
	}
	fc_tzentry->abbrev = pstrdup(fc_abbrev);

	fc_offset = strtok(NULL, WHITESPACE);
	if (!fc_offset)
	{
		GUC_check_errmsg("missing time zone offset in time zone file \"%s\", line %d",
						 fc_filename, fc_lineno);
		return false;
	}

	/* 我们假设区域名称不以数字或符号开头 */
	if (isdigit((unsigned char) *fc_offset) || *fc_offset == '+' || *fc_offset == '-')
	{
		fc_tzentry->zone = NULL;
		fc_tzentry->offset = strtol(fc_offset, &fc_offset_endptr, 10);
		if (fc_offset_endptr == fc_offset || *fc_offset_endptr != '\0')
		{
			GUC_check_errmsg("invalid number for time zone offset in time zone file \"%s\", line %d",
							 fc_filename, fc_lineno);
			return false;
		}

		fc_is_dst = strtok(NULL, WHITESPACE);
		if (fc_is_dst && pg_strcasecmp(fc_is_dst, "D") == 0)
		{
			fc_tzentry->is_dst = true;
			fc_remain = strtok(NULL, WHITESPACE);
		}
		else
		{
			/* 没有'D' dst说明符 */
			fc_tzentry->is_dst = false;
			fc_remain = fc_is_dst;
		}
	}
	else
	{
		/*
		 * 假设条目是区域名称。我们不尝试通过查找区域来验证它，
		 * 因为这将强制加载许多可能在当前会话中永远不会使用的区域。
		 */
		fc_tzentry->zone = pstrdup(fc_offset);
		fc_tzentry->offset = 0;
		fc_tzentry->is_dst = false;
		fc_remain = strtok(NULL, WHITESPACE);
	}

	if (!fc_remain)				/* 没有更多的非空格字符 */
		return true;

	if (fc_remain[0] != '#')		/* 必须是一个注释 */
	{
		GUC_check_errmsg("invalid syntax in time zone file \"%s\", line %d",
						 fc_filename, fc_lineno);
		return false;
	}
	return true;
}

/*
 * 将条目插入到已排序数组中
 *
 * *base: 数组的基地址（如果必须扩大数组则可更改）
 * *arraysize: 数组的分配长度（如果必须扩大数组则可更改）
 * n: 数组中当前有效元素的数量
 * entry: 要插入的新数据
 * override: 如果可以覆盖则为true
 *
 * 返回新数组长度（n的新值），如果出错则返回-1
 */
static int fc_addToArray(tzEntry **fc_base, int *fc_arraysize, int fc_n,
		   tzEntry *fc_entry, bool fc_override)
{
	tzEntry    *fc_arrayptr;
	int			fc_low;
	int			fc_high;

	/*
	 * 在数组中搜索重复项；作为一种有用的副作用，数组
	 * 保持排序状态。我们使用strcmp()来确保匹配
	 * datetime.c期望的排序顺序。
	 */
	fc_arrayptr = *fc_base;
	fc_low = 0;
	fc_high = fc_n - 1;
	while (fc_low <= fc_high)
	{
		int			fc_mid = (fc_low + fc_high) >> 1;
		tzEntry    *fc_midptr = fc_arrayptr + fc_mid;
		int			fc_cmp;

		fc_cmp = strcmp(fc_entry->abbrev, fc_midptr->abbrev);
		if (fc_cmp < 0)
			fc_high = fc_mid - 1;
		else if (fc_cmp > 0)
			fc_low = fc_mid + 1;
		else
		{
			/*
			 * 找到了重复条目；除非相同，否则抱怨。
			 */
			if ((fc_midptr->zone == NULL && fc_entry->zone == NULL &&
				 fc_midptr->offset == fc_entry->offset &&
				 fc_midptr->is_dst == fc_entry->is_dst) ||
				(fc_midptr->zone != NULL && fc_entry->zone != NULL &&
				 strcmp(fc_midptr->zone, fc_entry->zone) == 0))
			{
				/* 返回未更改的数组 */
				return fc_n;
			}
			if (fc_override)
			{
				/* 相同缩写但有不同之处，覆盖 */
				fc_midptr->zone = fc_entry->zone;
				fc_midptr->offset = fc_entry->offset;
				fc_midptr->is_dst = fc_entry->is_dst;
				return fc_n;
			}
			/* 相同缩写但有不同之处，抱怨 */
			GUC_check_errmsg("time zone abbreviation \"%s\" is multiply defined",
							 fc_entry->abbrev);
			GUC_check_errdetail("Entry in time zone file \"%s\", line %d, conflicts with entry in file \"%s\", line %d.",
								fc_midptr->filename, fc_midptr->lineno,
								fc_entry->filename, fc_entry->lineno);
			return -1;
		}
	}

	/*
	 * 没有匹配项，在“low”位置插入。
	 */
	if (fc_n >= *fc_arraysize)
	{
		*fc_arraysize *= 2;
		*fc_base = (tzEntry *) repalloc(*fc_base, *fc_arraysize * sizeof(tzEntry));
	}

	fc_arrayptr = *fc_base + fc_low;

	memmove(fc_arrayptr + 1, fc_arrayptr, (fc_n - fc_low) * sizeof(tzEntry));

	memcpy(fc_arrayptr, fc_entry, sizeof(tzEntry));

	return fc_n + 1;
}

/*
 * 解析单个时区缩写文件---可以递归处理@INCLUDE
 *
 * filename: 用户指定的文件名（不包括路径）
 * depth: 当前递归深度
 * *base: 结果数组（如果必须扩大数组则可更改）
 * *arraysize: 数组的分配长度（如果必须扩大数组则可更改）
 * n: 数组中当前有效元素的数量
 *
 * 返回新数组长度（n的新值），如果出错则返回-1
 */
static int fc_ParseTzFile(const char *fc_filename, int fc_depth,
			tzEntry **fc_base, int *fc_arraysize, int fc_n)
{
	char		fc_share_path[MAXPGPATH];
	char		fc_file_path[MAXPGPATH];
	FILE	   *fc_tzFile;
	char		fc_tzbuf[1024];
	char	   *fc_line;
	tzEntry		fc_tzentry;
	int			fc_lineno = 0;
	bool		fc_override = false;
	const char *fc_p;

	/*
	 * 我们强制要求文件名必须是全部字母字符。这可能是
	 * 过于严格，但我们不想允许访问任何
	 * 超出timezonesets目录的内容，因此，例如'/' *必须*
	 * 被拒绝。
	 */
	for (fc_p = fc_filename; *fc_p; fc_p++)
	{
		if (!isalpha((unsigned char) *fc_p))
		{
			/* 在级别0，只需使用guc.c的常规“无效值”消息 */
			if (fc_depth > 0)
				GUC_check_errmsg("invalid time zone file name \"%s\"",
								 fc_filename);
			return -1;
		}
	}

	/*
	 * 最大递归深度是一个相当任意的设置。很难
	 * 想象有人需要超过3级，因此在有人抱怨之前，
	 * 保持这个保守的设置。
	 */
	if (fc_depth > 3)
	{
		GUC_check_errmsg("time zone file recursion limit exceeded in file \"%s\"",
						 fc_filename);
		return -1;
	}

	get_share_path(my_exec_path, fc_share_path);
	snprintf(fc_file_path, sizeof(fc_file_path), "%s/timezonesets/%s",
			 fc_share_path, fc_filename);
	fc_tzFile = AllocateFile(fc_file_path, "r");
	if (!fc_tzFile)
	{
		/*
		 * 检查问题不在于文件名而在于目录。
		 * 这很值得关注，因为如果安装共享/
		 * 目录缺失或无法读取，这很可能是我们在
		 * postmaster 启动期间注意到问题的首个地方。
		 */
		int			fc_save_errno = errno;
		DIR		   *fc_tzdir;

		snprintf(fc_file_path, sizeof(fc_file_path), "%s/timezonesets",
				 fc_share_path);
		fc_tzdir = AllocateDir(fc_file_path);
		if (fc_tzdir == NULL)
		{
			GUC_check_errmsg("could not open directory \"%s\": %m",
							 fc_file_path);
			GUC_check_errhint("This may indicate an incomplete PostgreSQL installation, or that the file \"%s\" has been moved away from its proper location.",
							  my_exec_path);
			return -1;
		}
		FreeDir(fc_tzdir);
		errno = fc_save_errno;

		/*
		 * 否则，如果文件不存在且级别为 0，guc.c 的
		 * 报告就足够了。
		 */
		if (errno != ENOENT || fc_depth > 0)
			GUC_check_errmsg("could not read time zone file \"%s\": %m",
							 fc_filename);

		return -1;
	}

	while (!feof(fc_tzFile))
	{
		fc_lineno++;
		if (fgets(fc_tzbuf, sizeof(fc_tzbuf), fc_tzFile) == NULL)
		{
			if (ferror(fc_tzFile))
			{
				GUC_check_errmsg("could not read time zone file \"%s\": %m",
								 fc_filename);
				fc_n = -1;
				break;
			}
			/* 否则我们在 EOF 后来到这里 */
			break;
		}
		if (strlen(fc_tzbuf) == sizeof(fc_tzbuf) - 1)
		{
			/* 行对于 tzbuf 来说太长了 */
			GUC_check_errmsg("line is too long in time zone file \"%s\", line %d",
							 fc_filename, fc_lineno);
			fc_n = -1;
			break;
		}

		/* 跳过空白字符 */
		fc_line = fc_tzbuf;
		while (*fc_line && isspace((unsigned char) *fc_line))
			fc_line++;

		if (*fc_line == '\0')		/* 空行 */
			continue;
		if (*fc_line == '#')		/* 注释行 */
			continue;

		if (pg_strncasecmp(fc_line, "@INCLUDE", strlen("@INCLUDE")) == 0)
		{
			/* pstrdup 所以我们可以在结果数据结构中使用文件名 */
			char	   *fc_includeFile = pstrdup(fc_line + strlen("@INCLUDE"));

			fc_includeFile = strtok(fc_includeFile, WHITESPACE);
			if (!fc_includeFile || !*fc_includeFile)
			{
				GUC_check_errmsg("@INCLUDE without file name in time zone file \"%s\", line %d",
								 fc_filename, fc_lineno);
				fc_n = -1;
				break;
			}
			fc_n = fc_ParseTzFile(fc_includeFile, fc_depth + 1,
							fc_base, fc_arraysize, fc_n);
			if (fc_n < 0)
				break;
			continue;
		}

		if (pg_strncasecmp(fc_line, "@OVERRIDE", strlen("@OVERRIDE")) == 0)
		{
			fc_override = true;
			continue;
		}

		if (!fc_splitTzLine(fc_filename, fc_lineno, fc_line, &fc_tzentry))
		{
			fc_n = -1;
			break;
		}
		if (!fc_validateTzEntry(&fc_tzentry))
		{
			fc_n = -1;
			break;
		}
		fc_n = fc_addToArray(fc_base, fc_arraysize, fc_n, &fc_tzentry, fc_override);
		if (fc_n < 0)
			break;
	}

	FreeFile(fc_tzFile);

	return fc_n;
}

/*
 * load_tzoffsets --- 读取并解析指定的时区偏移文件
 *
 * 成功时，返回填充好的 TimeZoneAbbrevTable，该表必须是
 * 使用 malloc 分配而不是 palloc。失败时，返回 NULL，并使用 GUC_check_errmsg
 * 等提供问题的详细信息。
 */
TimeZoneAbbrevTable *
load_tzoffsets(const char *fc_filename)
{
	TimeZoneAbbrevTable *fc_result = NULL;
	MemoryContext fc_tmpContext;
	MemoryContext fc_oldContext;
	tzEntry    *fc_array;
	int			fc_arraysize;
	int			fc_n;

	/*
	 * 创建一个临时内存上下文进行工作。这使得后续清理变得简单。
	 */
	fc_tmpContext = AllocSetContextCreate(CurrentMemoryContext,
									   "TZParserMemory",
									   ALLOCSET_SMALL_SIZES);
	fc_oldContext = MemoryContextSwitchTo(fc_tmpContext);

	/* 将数组初始化为一个合理的大小 */
	fc_arraysize = 128;
	fc_array = (tzEntry *) palloc(fc_arraysize * sizeof(tzEntry));

	/* 解析文件 */
	fc_n = fc_ParseTzFile(fc_filename, 0, &fc_array, &fc_arraysize, 0);

	/* 如果到目前为止没有错误，允许 datetime.c 分配内存并转换格式 */
	if (fc_n >= 0)
	{
		fc_result = ConvertTimeZoneAbbrevs(fc_array, fc_n);
		if (!fc_result)
			GUC_check_errmsg("out of memory");
	}

	/* 清理 */
	MemoryContextSwitchTo(fc_oldContext);
	MemoryContextDelete(fc_tmpContext);

	return fc_result;
}
